# coding=utf-8
import io
import json
import os
import re
import sys
from datetime import datetime
from math import ceil
from pathlib import Path
from random import random
from typing import Any, Iterable, Union
from dateutil.parser import parse


projectDir = f'{Path(__file__,"../../../").resolve()}'
"""The parent directory of the node_modules."""


class FileName:
    fullName: str
    name: str

    def __init__(self, fullName: str, name: str) -> None:
        self.fullName = fullName
        self.name = name


class ProjectDir:
    projectDir = f'{Path(__file__,"../../../").resolve()}'
    """The parent directory of the node_modules."""

    def append(self, paths: str) -> Path:
        """在当前目录下append一个路径

        Args:
            paths (str): 在project之后的路径

        Returns:
            Path: project之后的path
        """
        return Path(self.projectDir, paths)

    def allFiles(self, paths: str, extName: str) -> list[FileName]:
        """遍历paths文件夹下所有文件

        Args:
            paths (str): 在工作文件夹目录之后的路径
            extName (str): 文件后缀

        Returns:
            list[str]: 该文件夹下所有的文件
        """
        dirname = f"{self.append(paths=paths).resolve()}"
        walker = os.walk(dirname)
        result: list[FileName] = []
        for _, _, files in walker:
            for file in files:
                if file.capitalize() .endswith(extName.capitalize()):
                    thisFileName = Path(dirname, file)
                    result.append(
                        FileName(fullName=f"{thisFileName}", name=file))

        return result


def getFileTimeString() -> str:
    date = datetime.now()
    return f"{keep0s(date.year)}-{keep0s(date.month,frontLen=2)}-{keep0s(date.day,frontLen=2)}@" + \
        f"{keep0s(date.hour,frontLen=2)}.{keep0s(date.minute,frontLen=2)}.{keep0s(date.second,frontLen=2)}"


def getUuid(prefix: str = "abc-", len=20) -> str:
    """Get a random string.

    Args:
        prefix (str, optional): Prefix. Defaults to "abc-".
        len (int, optional): Length. Defaults to 20.

    Returns:
        str: Random string.
    """    """"""
    for i in range(len):
        rmd = round(random() * 35)
        if rmd < 10:
            prefix += f"{rmd}"
        else:
            asc = rmd - 10 + 97
            prefix += chr(asc)
    return prefix


def keep0s(src: Union[int, float, str], frontLen: int = 0, aftLen: int = 0) -> str:
    """Maintain the number of 0s in the front and the back of the number.

    Args:
        src (int or float or str): Number to be maintained.
        frontLen (int, optional): Number of 0s in the front. Defaults to 0.
        aftLen (int, optional):  Number of 0s in the back. Defaults to 0.

    Returns:
        str: Maintained number.

    example:
    >>> keep0s("1.1", 2, 3) == '01.100' # True
    """
    final_arr: list[str] = []  # 最终结果数组join到一块
    floated: float = float(src)
    is_minus: bool = floated < 0
    absStr = f"{abs( floated)}"  # 绝对值
    splitted: list[str] = absStr.split(".")
    if is_minus:
        final_arr = ["-"]+final_arr
    if len(splitted) >= 1:
        front: str = splitted[0]
        while len(front) < frontLen:
            front = "0"+front
        final_arr.append(front)
    if len(splitted) >= 2:
        aft: str = splitted[1]
        if aft != "0" or aftLen:
            while len(aft) < aftLen:
                aft += "0"
            final_arr.append(".")
            final_arr.append(aft)
    return "".join(final_arr)


def chunk(arr: Iterable, size: int = 5) -> list[list]:
    import pydash as _
    """Creates a list of elements split into groups the length of size.
    If array can't be split evenly, the final chunk will be the remaining elements.

    Args:
        arr (list): List to chunk.
        size (int, optional): Chunk size. Defaults to 5.

    Returns:
        list: New list containing chunks of array.

    Example:
        >>> chunk([1, 2, 3, 4, 5], 2)
        [[1, 2], [3, 4], [5]]
    """
    return _.chunk([*arr], size)  # type: ignore


def chunkCol(arr: Iterable, split_into: int = 3):
    """Split a list by specified number.

    Required packages:
        pydash

    Args:
        arr (list): List to chunk.
        split_into (int, optional): The length of chunk you want to split into. Defaults to 3.

    Returns:
        list: New list containing chunks of array.

    Example:
        >>> chunkCol([1, 2, 3, 4, 5], 2) # [[1, 2, 3], [4, 5]]
        chunkCol([1, 2], 3) # [[1], [2]]
    """
    _arr = [*arr]
    split_into = abs(split_into) or 1
    maxLen = ceil(len(_arr) / split_into)
    return chunk(_arr, maxLen)


def toPandas(rows: Union[list[list[Any]], list[dict[str, Any]]], colIndex: int = 0, dataStart: int = 1, titlesDictionary: dict[str, str] = {}):
    """Convert a list of lists or a list of dicts to a pandas dataframe.

    Required packages:
        pandas

    Args:
        rows (list[list]): List of lists or list of dicts.
        colIndex (int, optional): 0-based index of the column to be used. Defaults to 0.
        dataStart (int, optional): 0-based index of the first row to be used as data. Defaults to 1.
        titlesDictionary (dict[str, str], optional): Dictionary to translate column titles. Defaults to {}.

    Returns:
        pd.DataFrame: 返回该数据组成的pandas.DataFrame
    """
    import pandas as pd

    def transform(n: str):
        if n in titlesDictionary:
            return titlesDictionary[n]
        else:
            return n
    cols: list[str] = [f"{transform(str(f))}" for f in rows[colIndex]]
    if isinstance(rows[0], list):
        dictList = toDictList(rows, colIndex, dataStart,  # type: ignore
                              titlesDictionary)
        return pd.DataFrame(dictList, columns=cols)
    else:
        dictList = rows
        return pd.DataFrame(dictList, columns=cols)


class File:
    path = ""
    """The path of the file or directory."""
    isFile = False
    """Whether the path is a file."""
    isDir = False
    """Whether the path is a directory."""
    exist = False
    """Whether the file or directory exists."""

    def __init__(self, path: str):
        self.path = path
        self.isFile = os.path.isfile(path)
        self.isDir = os.path.isdir(path)
        self.exist = os.path.exists(path)


def walkSync(dirPath: str, depth=0, includeFiles: bool = True, includeDirs: bool = False) -> list[File]:
    """Find all files and/or directories in a directory,
    including subdirectories depending on the depth.

    Args:
        dirPath (str): Directory path.
        depth (int, optional): Depth. Defaults to 0.
        includeFiles (bool, optional): Whether to include files. Defaults to True.
        includeDirs (bool, optional): Whether to include directories. Defaults to False.

    Returns:
        list[File]: List of files and/or directories.
    """
    fileList = []
    for fileName in os.listdir(dirPath):  # 遍历该目录的每个文件名
        _curPath = Path(dirPath, fileName).resolve()  # 文件对饮的路径
        path_str = f"{_curPath}"
        if _curPath.is_file():
            # 如果是文件
            if includeFiles:
                fileList.append(path_str)
        elif _curPath.is_dir():
            # 如果是目录
            if includeDirs:
                fileList.append(path_str)  # 加入到list中
            if depth > 0:
                newWalk = walkSync(path_str, depth - 1,
                                   includeFiles, includeDirs)
                fileList.extend([f"{f.path}"for f in newWalk])
    sorted(fileList, key=os.path.getctime)  # 根据创建时间正序排列
    return [File(f) for f in fileList]


def toDate(dateStr: Union[str, float, int, datetime]) -> datetime:
    """Convert a date string or number to a datetime object.

    Args:
        dateStr (Union[str, float, int, datetime]): Date string or number.

    Returns:
        datetime: Datetime object.
    """
    date = datetime.now()
    ymdChinese = r"^\d年/\d月/\d日$"
    if isinstance(dateStr, float) or isinstance(dateStr, int):
        date = datetime.fromtimestamp(dateStr)
    elif isinstance(dateStr, str):
        if re.match(ymdChinese, dateStr, re.I):
            date = datetime.strptime(dateStr, r'%Y年%m月%d日')
        else:
            date = parse(dateStr)
    else:
        date = dateStr

    return date


def another(anyObj: Any) -> Any:
    import pydash as _
    """Get a copy of an object.

    Required packages:
        pydash

    Args:
        anyObj (object): Any object.

    Returns:
        object: Copy of the object.
    """
    return _.clone_deep(anyObj)


def ensureDir(dirPath: str) -> bool:
    """Ensure that a directory exists.

    Args:
        dirPath (str): Directory path.

    Returns:
        bool: Whether the directory exists before it is ensured.
    """
    # 去除首位空格
    dirPath = dirPath.strip()
    # 去除尾部 \ 符号
    dirPath = dirPath.rstrip("\\")
    # 判断路径是否存在
    # 存在     True
    # 不存在   False
    isExists = os.path.exists(dirPath)
    # 判断结果
    if not isExists:
        # 如果不存在则创建目录,创建目录操作函数
        os.makedirs(dirPath)
        return True
    else:
        # 如果目录存在则不创建，并提示目录已存在
        return False


def ensureFile(path: str) -> bool:
    """Ensure that a file exists.

    Args:
        path (str): File path.

    Returns:
        bool: Whether the file exists before it is ensured.
    """
    # 去除首位空格
    path = path.strip()
    # 去除尾部 \ 符号
    path = path.rstrip("\\")
    # 判断路径是否存在
    # 存在     True
    # 不存在   False
    isExists = os.path.exists(path)
    ensureDir(os.path.dirname(path))
    # 判断结果
    if not isExists:
        # 如果不存在则创建目录,创建目录操作函数
        with open(path, "w+", encoding="utf-8") as f:
            f.write("")
            f.close()
        return True
    else:
        # 如果目录存在则不创建，并提示目录已存在
        return False


def writeJson(path: str, obj: Any):
    """Write a json object to a file, with encoding utf-8.

    Args:
        path (str): File path.
        obj (Any): Object to be written.
    """
    dirPath = os.path.dirname(path)
    ensureDir(dirPath)
    with open(path, "w+", encoding="utf-8") as f:
        try:
            text = json.dumps(obj, ensure_ascii=False, indent="\t")
            f.write(text)
        finally:
            f.close()


def readJson(path: str) -> Any:
    """Read a json object from a file, with encoding utf-8.

    Args:
        path (str): File path.

    Returns:
        Any: Object read.
    """
    with open(path, "r", encoding="utf-8") as f:
        try:
            return json.load(f)
        finally:
            f.close()


def dateFormat(fmt: str = "yyyy-mm-dd", date: Union[datetime, str, int, float] = datetime.now()) -> str:
    """Format a date.

    Args:
        fmt (str, optional): Format string. Defaults to "yyyy-mm-dd".
        date (Union[datetime, str, int, float], optional): _description_. Defaults to datetime.now().

    Returns:
        str: Formatted date.
    """
    def createOpt(date0: datetime):
        return {
            "[Yy]+": f"{date0.year}",
            "m+": f"{date0.month}",
            "d+": f"{date0.day}",
            "H+": f"{date0.hour}",
            "M+": f"{date0.minute}",
            "S+": f"{date0.second}"
        }
    _dataParsed = toDate(date)
    opt = createOpt(_dataParsed)
    for k in opt:
        reg = re.compile(f"({k})")
        ret = re.findall(reg, fmt)
        for target in ret:
            fmt = fmt.replace(target, keep0s(opt[k], len(target)))
    return fmt


def toDictList(rows: list[list], colIndex: int = 0, dataStart: int = 1, titlesDictionary: dict[str, str] = {}) -> list[dict[str, Any]]:
    """Convert a list of lists to a list of dictionaries.

    Args:
        values (list[list]): List of lists.
        titleIndex (int, optional): 0-based index of the row of column titles. Defaults to 0.
        dataStart (int, optional): 0-based index of the first row to be used as data. Defaults to 1.
        titlesDictionary (dict[str, str], optional): Dictionary of column titles. Defaults to {}.

    Returns:
        list[dict]: 由字典组成的数组
    """
    cols: list[str] = [f"{f}" for f in rows[colIndex]]
    finalRows = []
    for rowRaw in rows[dataStart:]:
        rowTransformed = {}
        for i, col in enumerate(cols):
            if col in titlesDictionary:
                col = titlesDictionary[col]
            if i < len(rowRaw):
                rowTransformed[col] = rowRaw[i]
            else:
                rowTransformed[col] = None
        finalRows.append(rowTransformed)
    return finalRows


def fromDictList(rows: list[dict[str, Any]], withTitles: bool = True, titles: Union[list[str], None] = None) -> list[list[Any]]:
    import pydash as _
    """Convert a list of dicts to a list of lists.

    Required packages:
        pydash

    Args:
        rows (list[dict[str, Any]]): A list of dicts.
        withTitles (bool, optional): Whether to include titles. Defaults to True.
        titles (Union[list[str], None], optional): Defines the sequence of titles if required. Defaults to None.

    Returns:
        list[list[Any]]: _description_
    """
    _cols: list[str] = []
    finalArr: list[list[Any]] = []
    if titles is None:
        _cols = _.keys(rows[0])
    else:
        _cols = titles
    for row in rows:
        finalArr.append(
            [row[col] for col in _cols]
        )
    if withTitles:
        return [_cols, *finalArr]
    else:
        return finalArr


def monthDifferential(time0: Union[str, float, int, datetime], time1: Union[str, float, int, datetime], exact: bool = False) -> Union[int, float]:
    """Get the month differential between two datetimes.

    Args:
        time0 (Union[str, float, int, datetime]): The datetime to be minused.
        time1 (Union[str, float, int, datetime]):  The datetime to be subtracted.

    Returns:
        Union[int, float]: The month differential.
    """
    date0 = toDate(time0)
    date1 = toDate(time1)
    if exact:
        return date0.timestamp() - date1.timestamp()
    else:
        return (date0.year - date1.year) * 12 + (date0.month - date1.month)


def returns(msg: Any) -> None:
    """Returns a message to the 'runScript' function of the nodejs package 'albert.gao'

    For details, see: https://gitee.com/AlbertGao/albertgao-common/blob/master/docs/modules.md#runscript

    Args:
        result (Any): The message to be returned.

    Returns:
        None
    """
    print(f"<nodejs-result>{json.dumps(msg)}</nodejs-result>")


def receive() -> Any:
    """Receive a message from the 'runScript' function of the nodejs package 'albert.gao'

    For details, see: https://gitee.com/AlbertGao/albertgao-common/blob/master/docs/modules.md#runscript

    Returns:
        Any: The msgTo parameter of the 'runScript' function.
    """
    sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
    return json.loads(sys.stdin.read())
